Skip to content

Fix U2FMigrator specs failing with OpenSSL v3.3+#468

Merged
santiagorodriguez96 merged 2 commits intomasterfrom
sr--fix-u2f-specs
Sep 17, 2025
Merged

Fix U2FMigrator specs failing with OpenSSL v3.3+#468
santiagorodriguez96 merged 2 commits intomasterfrom
sr--fix-u2f-specs

Conversation

@santiagorodriguez96
Copy link
Copy Markdown
Contributor

@santiagorodriguez96 santiagorodriguez96 commented Jun 26, 2025

Fixes #463.

Fixed this by using the FIDO U2F Attestation with ES256 Credential test vector provided in WebAuthnAPI specification. I did change the rpIdHash of the assertion from https://exmple.org to https://exmple.org/appid, so we can keep testing for the appid being different than the origin.

This is the script that I used for obtaining the values from the test vector and changing the `rpIdHash`
require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "webauthn"
  gem "base64"
  gem "pry-byebug"
  gem "minitest"
  gem "activesupport"
end

require "webauthn"
require "webauthn/u2f_migrator"
require "base64"
require "pry-byebug"
require "active_support"
require "minitest/autorun"

class BugTest < ActiveSupport::TestCase
  def test_1
    WebAuthn.configuration.allowed_origins = ["https://example.org"]
    app_id = "https://example.org/appid"

    # Data from https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#sctn-test-vectors-fido-u2f-es256
    w3c_u2f_test_vector = {
      registration: {
        attestation_private_key: ['51bd002938fa10b83683ac2a2032d0a7338c7f65a90228cfd1f61b81ec7288d0'].pack('H*'),
        credential_id: ['a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4'].pack('H*'),
        client_data_json: ['7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22344851334b5a4335797155486f696666786e73414e344445557955344452715177672d4237583049444159222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d'].pack('H*'),
        attestation_object: ['a363666d74686669646f2d7532666761747453746d74a26373696758473045022100f41887a20063bb26867cb9751978accea5b81791a68f4f4dd6ea1fb6a5c086c302204e5e00aa3895777e6608f1f375f95450045da3da57a0e4fd451df35a31d2d98a637835638159022530820221308201c7a003020102021004f66dc6542ea7719dea416d325a2401300a06082a8648ce3d0403023062311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331253023060355040b0c1c41757468656e74696361746f72204174746573746174696f6e204341310b30090603550406130241413020170d3234303130313030303030305a180f33303234303130313030303030305a305f311e301c06035504030c15576562417574686e207465737420766563746f7273310c300a060355040a0c0357334331223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e310b30090603550406130241413059301306072a8648ce3d020106082a8648ce3d0301070342000456fffa7093dede46aefeefb6e520c7ccc78967636e2f92582ba71455f64e93932dff3be4e0d4ef68e3e3b73aa087e26a0a0a30b02dc2aa2309db4c3a2fc936dea360305e300c0603551d130101ff04023000300e0603551d0f0101ff040403020780301d0603551d0e04160414420822eb1908b5cd3911017fbcad4641c05e05a3301f0603551d2304183016801445aff715b0dd786741fee996ebc16547a3931b1e300a06082a8648ce3d040302034800304502200d0b777f0a0b181ad2830275acc3150fd6092430bcd034fd77beb7bdf8c2d546022100d4864edd95daa3927080855df199f1717299b24a5eecefbd017455a9b934d8f668617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b54100000000afb3c2efc054df425013d5c88e79c3c10020a4ba6e2d2cfec43648d7d25c5ed5659bc18f2b781538527ebd492de03256bdf4a5010203262001215820b0d62de6b30f86f0bac7a9016951391c2e31849e2e64661cbd2b13cd7d5508ad225820503b0bda2a357a9a4b34475a28e65b660b4898a9e3e9bbf0820d43494297edd0'].pack('H*'),
      },
      authentication: {
        challenge: ['f90c612981d84f599438de1a500f76926e92cc84bef8e02c6e23553f00485435'].pack('H*'),
        authenticator_data: ['bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50100000000'].pack('H*'),
        client_data_json: ['7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a222d5178684b59485954316d554f4e3461554139326b6d36537a49532d2d4f417362694e5650774249564455222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d'].pack('H*'),
      }
    }

    key_handle = Base64.urlsafe_encode64(w3c_u2f_test_vector[:registration][:credential_id])

    attestation_object = WebAuthn::AttestationObject.deserialize(w3c_u2f_test_vector[:registration][:attestation_object], WebAuthn.configuration.relying_party)

    certificate = Base64.strict_encode64(attestation_object.attestation_statement.attestation_certificate.to_der)

    credential_public_key = WebAuthn::AttestationStatement::FidoU2f::PublicKey.new(
      attestation_object.authenticator_data.attested_credential_data.credential.public_key
    ).to_uncompressed_point
    encoded_credential_public_key = Base64.strict_encode64(credential_public_key)

    migrated_credential =
      WebAuthn::U2fMigrator.new(
        app_id: app_id,
        certificate: certificate,
        key_handle: key_handle,
        public_key: encoded_credential_public_key,
        counter: 41
      )

    original_authenticator_data = WebAuthn::AuthenticatorData.deserialize(w3c_u2f_test_vector[:authentication][:authenticator_data])

    authenticator_data = WebAuthn::FakeAuthenticator::AuthenticatorData.new(
      rp_id_hash: OpenSSL::Digest::SHA256.digest(app_id), # change the original rp_id_hash in order to include the `/appid` path
      credential: original_authenticator_data.attested_credential_data_included? && { id: original_authenticator_data.credential.id, public_key: original_authenticator_data.credential.public_key },
      sign_count: original_authenticator_data.sign_count,
      user_present: original_authenticator_data.user_present?,
      user_verified: original_authenticator_data.user_verified?,
      backup_eligibility: original_authenticator_data.credential_backup_eligible?,
      backup_state: original_authenticator_data.credential_backed_up?,
      aaguid: original_authenticator_data.attested_credential_data_included? && original_authenticator_data.aaguid,
      extensions: original_authenticator_data.extension_data_included? && original_authenticator_data.extension_data,
    ).serialize

    client_data_json = w3c_u2f_test_vector[:authentication][:client_data_json]
    client_data_hash = OpenSSL::Digest::SHA256.digest(client_data_json)

    credential_key_group = OpenSSL::PKey::EC::Group.new(migrated_credential.send(:credential_cose_key).curve.pkey_name)
    credential_public_key_point = OpenSSL::PKey::EC::Point.new(credential_key_group, migrated_credential.send(:credential_cose_key).to_pkey.public_key.to_bn)
    asn1 = OpenSSL::ASN1::Sequence(
      [
        OpenSSL::ASN1::Integer.new(1),
        OpenSSL::ASN1::OctetString(['51bd002938fa10b83683ac2a2032d0a7338c7f65a90228cfd1f61b81ec7288d0'].pack('H*')),
        OpenSSL::ASN1::ObjectId(migrated_credential.send(:credential_cose_key).curve.pkey_name, 0, :EXPLICIT),
        OpenSSL::ASN1::BitString(credential_public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT),
      ]
    )
    credential_key = OpenSSL::PKey::EC.new(asn1.to_der)

    signature = credential_key.sign('SHA256', authenticator_data + client_data_hash)

    puts "Certificate: #{certificate}"
    puts "Key handle: #{key_handle}"
    puts "Public key: #{encoded_credential_public_key}"
    puts "Assertion challenge: #{Base64.strict_encode64(w3c_u2f_test_vector[:authentication][:challenge])}"
    puts "Client data json: #{Base64.strict_encode64(client_data_json)}"
    puts "Credential public key: #{Base64.strict_encode64(credential_key.to_pem)}"
    puts "Signature: #{Base64.strict_encode64(signature)}"
    puts "Authenticator data: #{Base64.strict_encode64(authenticator_data)}"

    assert WebAuthn::AuthenticatorAssertionResponse.new(
      client_data_json: client_data_json,
      authenticator_data: authenticator_data,
      signature: signature,
    ).valid?(
      w3c_u2f_test_vector[:authentication][:challenge],
      public_key: migrated_credential.credential.public_key,
      sign_count: 0,
      rp_id: app_id
    )
  end

  <<~OUTPUT
    # Running:

    Certificate: MIICITCCAcegAwIBAgIQBPZtxlQup3Gd6kFtMlokATAKBggqhkjOPQQDAjBiMR4wHAYDVQQDDBVXZWJBdXRobiB0ZXN0IHZlY3RvcnMxDDAKBgNVBAoMA1czQzElMCMGA1UECwwcQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbiBDQTELMAkGA1UEBhMCQUEwIBcNMjQwMTAxMDAwMDAwWhgPMzAyNDAxMDEwMDAwMDBaMF8xHjAcBgNVBAMMFVdlYkF1dGhuIHRlc3QgdmVjdG9yczEMMAoGA1UECgwDVzNDMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJBQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFb/+nCT3t5Grv7vtuUgx8zHiWdjbi+SWCunFFX2TpOTLf875ODU72jj47c6oIfiagoKMLAtwqojCdtMOi/JNt6jYDBeMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBRCCCLrGQi1zTkRAX+8rUZBwF4FozAfBgNVHSMEGDAWgBRFr/cVsN14Z0H+6ZbrwWVHo5MbHjAKBggqhkjOPQQDAgNIADBFAiANC3d/CgsYGtKDAnWswxUP1gkkMLzQNP13vre9+MLVRgIhANSGTt2V2qOScICFXfGZ8XFymbJKXuzvvQF0Vam5NNj2
    Key handle: pLpuLSz-xDZI19JcXtVlm8GPK3gVOFJ-vUkt4DJWvfQ=
    Public key: BLDWLeazD4bwusepAWlRORwuMYSeLmRmHL0rE819VQitUDsL2io1eppLNEdaKOZbZgtImKnj6bvwgg1DSUKX7dA=
    Assertion challenge: +QxhKYHYT1mUON4aUA92km6SzIS++OAsbiNVPwBIVDU=
    Client data json: eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiLVF4aEtZSFlUMW1VT040YVVBOTJrbTZTeklTLS1PQXNiaU5WUHdCSVZEVSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5vcmciLCJjcm9zc09yaWdpbiI6ZmFsc2V9
    Credential public key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUZHOUFDazQraEM0Tm9Pc0tpQXkwS2N6akg5bHFRSW96OUgyRzRIc2NvalFvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFc05ZdDVyTVBodkM2eDZrQmFWRTVIQzR4aEo0dVpHWWN2U3NUelgxVkNLMVFPd3ZhS2pWNgpta3MwUjFvbzVsdG1DMGlZcWVQcHUvQ0NEVU5KUXBmdDBBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
    Signature: MEUCID6+MZ0WQ1rW9deqfQbw8LR7/zMvvUnMfAAmgDF2ksUmAiEApgp546w47JGxWS38AC/GznH9fcYUx5Zva62N+1KCg+c=
    Authenticator data: HIcRkL9v+l6dYGTcxfrXQ/usPPfCIO8wnPdFumKDZbcBAAAAAA==
    .

    Finished in 0.002110s, 473.9336 runs/s, 473.9336 assertions/s.

    1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
  OUTPUT
end

@santiagorodriguez96 santiagorodriguez96 force-pushed the sr--fix-u2f-specs branch 2 times, most recently from 7e0981e to 562aa63 Compare August 13, 2025 17:20
@santiagorodriguez96 santiagorodriguez96 marked this pull request as ready for review August 13, 2025 17:20
Copy link
Copy Markdown
Member

@nicolastemciuc nicolastemciuc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! ❤️

@santiagorodriguez96 santiagorodriguez96 merged commit a48c2db into master Sep 17, 2025
11 checks passed
@santiagorodriguez96 santiagorodriguez96 deleted the sr--fix-u2f-specs branch September 19, 2025 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebAuthn::U2fMigrator returns the attestation certificate spec failing with OpenSSL v3.3+

2 participants